package de.pyranja.storage.core;

import java.util.concurrent.ConcurrentMap;

import javax.annotation.concurrent.NotThreadSafe;

import org.apache.jdbm.DB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.pyranja.storage.exception.BlobNotFoundException;

/**
 * <p>The archive records all stored blobs and keeps track of the live references
 * to each blob.</p>
 * <p>Due to inconsistencies in the face of concurrent addition and removal, the
 * archive is <strong>not</strong> thread-safe.</p>
 * @author Chris Borckholder
 */
@NotThreadSafe
public class Archive {
	/* slf4j-logger */
	private final static Logger log = LoggerFactory.getLogger(Archive.class);
	
	final static String DATABASE = "blob_records";
	
	/* backing jdbm */
	private final DB database;

	private ConcurrentMap<String,Integer> records;
	
	/**
	 * Instantiates an archive using the given backing JDBM.
	 * @param database to use
	 */
	public Archive(final DB database) {
		// database may not be initialized - DB will silently return null
		// check for that and create it if necessary
		records = database.getHashMap(DATABASE);
		if (records == null) {
			log.info("creating backing database as {}", DATABASE);
			records = database.createHashMap(DATABASE);
		}
		this.database = database;
	}
	
	/**
	 * Lifecycle method. Closes the backing database of this archive.
	 * @throws IllegalStateException if archive was already shut down
	 */
	public void shutdown() {
		log.info("shutting down {}", this);
		if (database.isClosed()) {
			// to avoid corrupting the application's shutdown only log a warning
			log.warn("Archive's backing database {} has already been closed." +
					"Check lifecycle management.", database);
		}
		database.commit();
		database.close();
	}

	/**
	 * Adds a reference to the given blob if it exists.
	 * @param id of blob
	 * @throws BlobNotFoundException if id is not referenced yet
	 * @return current references to given blob
	 */
	public int addIfExisting(final String id) {
		log.trace("add if exists {}",id);
		Integer counter = records.get(id);
		if (counter != null) {
			records.put(id, ++counter);
		} else {
			throw new BlobNotFoundException(id);
		}
		assert(counter > 0);	// integrity
		log.trace("added to existing {} now at {}", id, counter);
		return counter;
	}
	
	/**
	 * Adds a reference to the given blob. If there is none present yet, a new
	 * reference counter is created and set to 1.
	 * @param id of blob
	 * @return current references to given blob
	 */
	public int addOrInit(final String id) {
		/*
		 * Forces an increase, if necessary creating a new entry for the blob id
		 */
		log.trace("forced add {}",id);
		Integer counter = records.get(id);
		if (counter != null) {
			records.put(id, ++counter);
		} else {
			counter = Integer.valueOf(1);
			records.put(id, counter);
			log.debug("initialized record for {} to be {}", id, counter);
		}
		assert(counter > 0); // integrity
		log.trace("forced added {} now at {}", id, counter);
		return counter;
	}

	/**
	 * Removes a reference to the given blob. If no references remain, the blob
	 * record is removed.
	 * @param id of blob
	 * @return remaining number of references
	 */
	public int release(final String id) {
		log.trace("releasing {}", id);
		Integer counter = records.get(id);
		if (counter != null) {
			if (--counter < 1) {
				log.debug("removing record for {} at {} references", id, counter);
				records.remove(id);
			} else {
				records.put(id, counter);
			}
		} else {
			log.debug("found no record for {} on release", id);
			counter = Integer.valueOf(0);
		}
		log.trace("released {} now at {}", id, counter);
		return counter;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "Archive []";
	}
}